Skip to content

feat: Block segment publish action for a streaming task until prior task groups are complete - #20505

Merged
kfaraz merged 18 commits into
apache:masterfrom
kfaraz:segment_publish_queue
Oct 14, 2026
Merged

kfaraz merged 18 commits into
apache:masterfrom
kfaraz:segment_publish_queue

Conversation

@kfaraz

@kfaraz kfaraz commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

Follow up to #19091 as considered in #19091 (comment).

The changes in #19091 have shown that tasks often need to wait for their priors to finish publishing as we often see logs like:

Task[xyz] needs to wait before publishing as other taskGroups[abc, def]
are currently publishing to partition[KafkaTopicPartition{partition=1, topic='null', multiTopicPartition=false}].

Before this patch, if publish from streaming task A is slow, a later generation task B keeps trying to publish repeatedly until eventually failing once the number of retries is exhausted.
It is difficult to fix the number of retries since publish times may vary across supervisors and over time.
It is also pointless to retry if we already know that a prior task is yet to finish publishing.

This patch tries to address these problems by blocking the task action of the later task B until A is complete.

Changes

  • Add TaskActionClient.submitAsync which returns a ListenableFuture
  • Update API /druid/indexer/v1/action in OverlordResource to be async by employing the new submitAsync() method
  • Update task actions segmentTransactionalAppend and segmentTransactionalReplace to implement performAsync()
  • Update LocalTaskActionClient.submitAsync() to use performAsync() when the task action supports it
  • The segment publish is now attempted only when the task is ready to publish
  • The API response is sent only once the publish attempt is done
  • For non-streaming tasks, publish is attempted right away
  • Add method isTaskReadyToPublish() in SeekableStreamSupervisor.
    • This method returns a ListenableFuture that completes when all prior task groups have completed

Other changes

  • Update SegmentAllocateAction and SegmentAllocationQueue to use ListenableFuture to align with the new signature of performAsync()

Follow up items

  • Emit metric from SeekableStreamSupervisor and include the waiting tasks in the supervisor report to allow debugging in case a task remains stuck waiting to publish
  • Add more tests for new methods
  • Tie task action timeout to supervisor completion timeout

This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Address the async response lifecycle failures before merging: ordinary actions returning null fail before serialization, and completed response writes never close the async request. The readiness path also has a completion-registration race and can run database publication on the supervisor's single-threaded notice executor.

Reviewed all 17 of 17 changed files, including the surrounding task-action client, supervisor lifecycle, and metadata publication paths.

Validation: git diff --check 131989817271bc836e815db738cc2be33fba3538 HEAD passed. Static review only; no tests or builds were run.

Severity Findings
P0 0
P1 2
P2 2
P3 0
Total 4

This is an automated review by Codex GPT-5.6-Luna(max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

catch (Exception e) {
log.noStackTrace().warn(e, "Request timed out or is closed already");
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Complete the async response on every path

Finding: doAction starts an AsyncContext, but this helper only sets the status and optionally writes a body; it never completes the context. Consequently, even a finished task action leaves the HTTP exchange open until the async timeout, and the no-leader path returns here before installing any timeout listener.

Suggestion: Complete the async context after the response is written, including when no response body is present.

// It would be great to verify that this worker is actually supposed to be running the task before
// actually doing the action. Some ideas for how that could be done would be using some sort of attempt_id
// or token that gets passed around.
completeRequest(asyncContext, Status.OK.getStatusCode(), Map.of("result", result));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve null task-action results

Finding: Several existing actions, including LockReleaseAction and UpdateLocationAction, implement TaskAction<Void> and return null. Map.of rejects null values, so these successful actions throw from onSuccess before completeRequest can send the result, leaving the async request unanswered.

Suggestion: Build the response with a null-tolerant map so the existing result: null response contract is preserved.

for (TaskGroup group : blockingTaskGroups.keySet()) {
group.addCompletionListener(() -> {
latch.countDown();
blockingTaskGroups.remove(group);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid mutating the map being iterated

Finding: The constructor iterates blockingTaskGroups.keySet() while each completion callback removes from that same constructor argument. If a prior group completes during registration, addCompletionListener can run the callback inline (or concurrently), modifying the HashMap during iteration and throwing ConcurrentModificationException before the readiness future is returned.

Suggestion: Register listeners from an immutable snapshot and update a separate thread-safe tracking structure from the callbacks.

return SegmentPublishResult.retryableFailure("Task is not ready to publish yet");
}
},
MoreExecutors.directExecutor()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep publish work off the supervisor notice thread

Finding: The readiness future is completed by TaskGroup.onCompleted during checkPendingCompletionTasks, which runs on this supervisor's single-threaded notice executor. With a direct executor, this transform calls perform inline; that method enters the task-lock critical section and performs metadata-store publication, so a slow publish blocks this supervisor from processing subsequent status checks and notices. The same executor choice is present in the insert action.

Suggestion: Dispatch the publish operation to a dedicated executor when readiness completes, leaving the supervisor notice thread free to process its queue.

@Fly-Style
Fly-Style self-requested a review October 8, 2026 15:01
@kfaraz
kfaraz requested a review from FrankChen021 October 9, 2026 08:13

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The async publish path still has failure and timeout handling gaps that can leave action requests hanging or fail a streaming task before the prior task group finishes.

Reviewed 18 of 18 changed files, including the task-action client and HTTP response lifecycle, supervisor readiness/listener state, metadata publication path, and affected tests.

Validation: git diff --check 9a792495bd7e5ecdf6e8621b71b5983d072c3081 HEAD passed. Static review only; no tests or builds were run.

Severity Findings
P0 0
P1 2
P2 1
P3 0
Total 3

This is an automated review by Codex GPT-5.6 Luna(Max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

public Response doAction(final TaskActionHolder holder)
public void doAction(
final TaskActionHolder holder,
@Nullable @PathParam("timeout") final Long timeoutMillis,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Make the async action timeout cover publish waits

Finding: The /action route has no {timeout} path segment, so @PathParam("timeout") is never populated and setTimeout always uses five minutes. The supervisor's default completionTimeout is 30 minutes; a legitimate pending task group that takes longer than five minutes causes the timeout listener to cancel the action and complete the request before the readiness future finishes. The worker then sees an I/O or empty-response failure, and the publish retry path does not retry that timeout, so the new blocking behavior can fail a task instead of waiting for the prior group.

Suggestion: Bind the timeout to a real request parameter or route and propagate a deadline that covers the configured supervisor completion wait, with an explicit retry-safe timeout response.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: this should be query param.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up assessment

Agreed, @QueryParam("timeout") is the right binding. That fixes the annotation typo, but the timeout concern also needs the caller/default addressed: RemoteTaskActionClient posts to /druid/indexer/v1/action without a timeout query parameter, so changing the annotation alone still leaves worker requests capped at five minutes. A prior task group may legitimately remain pending under the supervisor's default 30-minute completionTimeout; the listener cancels the action instead of returning a retryable SegmentPublishResult.

Suggestion: Have worker requests use a deadline that covers the configured supervisor completion wait (or make the server derive it), and return an explicit response that the publish caller can safely retry when that deadline expires. The existing P1 remains actionable at this head; no additional finding is raised by this follow-up.

Reviewed 7 repository source files for this thread: 3 of the PR's 18 changed files and 4 supporting files. This was a focused static follow-up, not a full PR re-review.


This is an automated review by Codex GPT-5.6-Luna(max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the parameter since the client is never sending it. A future PR may add a new field inside the task action payload itself. Alternatively, it may even be linked up to the completion timeout.

For now, on timeout, we send back a gateway timeout response so that the client retries (via StandardRetryPolicy).

@kfaraz
kfaraz requested a review from FrankChen021 October 9, 2026 13:19

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The current async action path still has two correctness and lifecycle problems before merge: the initial publish attempt can execute synchronously before the servlet timeout is installed, and timeout cancellation races the direct future callback, so the intended 504 can become a 500 or a duplicate completion.

Reviewed 18 of 18 changed files in the full merge-base diff, including the surrounding task-action, supervisor, metadata-transaction, HTTP, and test code. I also rechecked both prior inline review threads against the current head. This was a static review; no build or tests were run. Validation performed: git diff --check 9a792495bd7e5ecdf6e8621b71b5983d072c3081 HEAD passed, and the local read-only merge gate passed against master.

Severity Findings
P0 0
P1 2
P2 0
P3 0
Total 2

This is an automated review by Codex GPT-5.6 Luna(Max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two P1 async lifecycle races remain: the initial publish can run before timeout setup, and cancellation can trigger a 500 callback before the intended 504 timeout response.

Reviewed 18 of 18 changed files, including surrounding task-action, HTTP, supervisor, and metadata code. Static review only; no tests or builds were run.

Validation: git diff --check 9a792495bd7e5ecdf6e8621b71b5983d072c3081 HEAD passed.

Severity Findings
P0 0
P1 2
P2 0
P3 0
Total 2

This is an automated review by Codex GPT-5.6-Luna(max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

return;
}

final ListenableFuture<?> future = taskActionClient.get().submitAsync(holder.getAction());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Initial publish runs before timeout setup

Finding: submitAsync is invoked before the timeout listener and 15-minute timeout are registered. Local streaming publish actions execute their first metadata publish synchronously inside submitAsync, so a slow first attempt is governed by the container's default timeout and cannot be canceled through this listener.

Suggestion: Install the timeout and one-shot completion state before starting the action, or move the initial publish attempt off the request thread.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated.

@kfaraz
kfaraz requested a review from FrankChen021 October 13, 2026 12:25

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Do not merge as-is: the new publish-wait path can fail a valid local-runner streaming task after five minutes even though predecessor completion is allowed to take longer, and generic async action failures now lose the existing error payload and diagnostics.

Reviewed 22 of 22 changed files, including the full merge-base diff and the incremental changes since d0773ad6267212b66811f1daac09ca2ff003ac8c. Coverage included task-action async execution, segment publish and metadata paths, supervisor pending-group lifecycle, Overlord HTTP handling, and corresponding tests. The two referenced prior async-lifecycle findings were rechecked at the current head and are resolved; no useful inline reply remains.

Validation: git diff --check 9a792495bd7e5ecdf6e8621b71b5983d072c3081 HEAD passed. Static review only; no tests or builds were run.

Severity Findings
P0 0
P1 1
P2 1
P3 0
Total 2

This is an automated review by Codex GPT-5.6-Luna(max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

supervisorId,
task.getId(),
startMetadata
return Futures.transform(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Publish waits can fail local tasks after five minutes

Finding: Defect: this future can remain pending until prior task groups finish, but local callers invoke it through LocalTaskActionClient.submit, whose async branch waits only five minutes. Trigger: a predecessor remains pending or the publish itself exceeds five minutes; the default supervisor completion timeout is 30 minutes, and SequenceMetadata uses synchronous submit for this action. Impact: the local-runner streaming task receives a wrapped TimeoutException and fails instead of waiting for the predecessor or returning the intended retryable publish result.

Suggestion: Give streaming publish waits a deadline aligned with the supervisor completion window, or keep them out of the generic five-minute async-action path so the task can receive the publish result or retry status.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default timeout has no been increased to 15 minutes.
Moreover, on timeout, the task will retry. So this failure is okay.
A future patch may tie this up to the completion timeout.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The five-minute issue remains at the current head. SequenceMetadata still calls TaskActionClient.submit, and LocalTaskActionClient.performAction still waits on performAsync(...).get(5, TimeUnit.MINUTES), while the supervisor completion timeout defaults to 30 minutes. A valid predecessor wait can therefore fail the local streaming task after five minutes, and the timeout path does not cancel the pending future, so it can retain the listener and later attempt publication after the task has failed. Please align the action timeout with the supervisor completion window and cancel the future on timeout, or keep this long-lived wait out of the fixed synchronous path.

if (t instanceof DruidException druidException) {
asyncRequest.complete(druidException.getStatusCode(), druidException.toErrorResponse());
} else {
asyncRequest.complete(Status.INTERNAL_SERVER_ERROR.getStatusCode(), null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Generic async failures lose the error payload

Finding: Defect: the generic failure branch completes a bare 500 response, whereas the previous endpoint returned an error entity. Trigger: any action failure that is not delivered as a direct DruidException, including RuntimeException wrappers from transactional publish actions. Impact: remote task clients and operators receive an empty 500 and lose the actionable message and diagnostics previously exposed.

Suggestion: Preserve the previous JSON error payload and exception logging in the generic failure branch while retaining the DruidException-specific status handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response-payload part is fixed, but the generic failure branch still omits the previous log.warn(Throwable, ...) call. The client now receives the error message, yet the Overlord loses the stack trace and server-side diagnostics for generic action failures. Please restore throwable logging before completing the 500 response.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

🟡 Changes recommended

Do not merge as-is: the new readiness future can deadlock when multiple task generations are pending, and the local task path still fails a publish wait after five minutes while leaving the wait active. The incremental generic-error fix restores the response payload, but generic failures still lose the previous server-side stack trace.

Reviewed 22 of 22 changed files; findings in 3 files. Coverage included the full merge-base diff, the incremental diff since 05dd75df86e8e8744dc3d2a3a0d1efb41d391307, current-head code, surrounding task-action clients, supervisor pending-group lifecycle, metadata publication, Overlord HTTP handling, and tests. The three earlier async-lifecycle findings were rechecked and are resolved: synchronous action failures now become failed futures, timeout completion is once-guarded, and the initial publish is dispatched through the action executor.

Validation: git diff --check 9a792495bd7e5ecdf6e8621b71b5983d072c3081 7fe22363dacaf516a8df62c4c25dd447b9676f6d passed. Static review only; no builds or tests were run.

Severity Findings
P0 0
P1 2
P2 1
P3 0
Total 3

This is an automated review by Codex GPT-5.6-Luna(max)

After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.

Severity Findings
P0 0
P1 2
P2 1
P3 0
Total 3

partitionId
);
return true;
for (TaskGroup taskGroup : partitionIdToPublishingGroups.get(partitionId)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Later pending generations can deadlock

Finding: Once maxAllowedStops or taskCount allows more than one pending group, pendingCompletionTaskGroups can contain older and newer generations for the same partition. This loop adds every overlapping group except groups containing the task ID; it never filters to predecessors. If an older task is still pushing when a newer group reaches publish, the older publish can wait for the newer group while the newer publish waits for the older one. Neither can complete until the completion timeout removes both, stalling ingestion and causing task restarts.

Suggestion: Associate the task with its pending TaskGroup or generation and register listeners only for groups that precede it; add coverage with two pending generations and a delayed first publish.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was wondering about this myself. Let me check if it is possible to check on the generation of the task.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the method in SeekableStreamSupervisor to wait on older task groups only.

supervisorId,
task.getId(),
startMetadata
return Futures.transform(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Align the local publish wait timeout

Finding: Streaming transactional actions now return a future that may wait for predecessor completion, but SequenceMetadata still invokes TaskActionClient.submit and LocalTaskActionClient.performAction still waits on performAsync(...).get(5, TimeUnit.MINUTES). A predecessor can legitimately remain pending until the supervisor completion timeout, which defaults to 30 minutes, so a valid task fails after five minutes instead of continuing. The timeout does not cancel the future, leaving its listener active and allowing it to invoke publish after the task has failed.

Suggestion: Give this wait an action-specific deadline tied to the supervisor completion timeout and cancel it on local timeout, or keep the long wait out of the fixed five-minute synchronous path.

@kfaraz kfaraz Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SequenceMetadata invokes TaskActionClient.submit on a RemoteTaskActionClient which then submits the actual task action to the Overlord, where OverlordResource.performTaskAction uses LocalTaskActionClient.submitAsync.

if (t instanceof DruidException druidException) {
asyncRequest.complete(druidException.getStatusCode(), druidException.toErrorResponse());
} else {
final Map<String, String> payload = new HashMap<>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore generic action failure logging

Finding: The incremental fix restores the generic 500 error payload, but this branch still does not log the throwable. Generic async action failures therefore lose the stack trace and server-side diagnostics that the previous synchronous handler emitted with log.warn, even though the client receives the message.

Suggestion: Log the throwable in the generic onFailure branch before completing the response.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added back the log.

@kfaraz
kfaraz requested a review from FrankChen021 October 14, 2026 06:13

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Follow-up assessment: the three findings from the prior review are addressed at the current head. The readiness check now blocks only on pending task groups whose start offsets precede the candidate task's offsets, and the added test verifies that a later pending generation does not block the candidate. The generic asynchronous failure path logs the throwable again. The five-minute synchronous wait in LocalTaskActionClient is not on the production streaming publish path under review: SequenceMetadata uses RemoteTaskActionClient, and OverlordResource now invokes submitAsync for the action, so the async-capable transactional publish waits through its future instead of performAction(...).get(5, TimeUnit.MINUTES).

Reviewed 5 of 5 files changed since the previous review; rechecked all 3 prior findings and the surrounding production call sites in TaskActionClient, LocalTaskActionClient, RemoteTaskActionClient, SequenceMetadata, OverlordResource, and the supervisor pending-group lifecycle. No new actionable findings.

Validation: git diff --check 9a792495bd7e5ecdf6e8621b71b5983d072c3081 93a75b5f95a7560c8c4280511b7ffcdfae3587d4 passed. Static review only; no builds or tests were run.


This is an automated review by Codex GPT-5.6 Luna(Max)

@kfaraz

kfaraz commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews, @Fly-Style , @FrankChen021 !

@kfaraz
kfaraz merged commit 7c89096 into apache:master Oct 14, 2026
27 checks passed
@github-actions github-actions Bot added this to the 39.0.0 milestone Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants